%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2010-2022. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

%%
%% Stress tests of rwmutex implementation.
%%
%% Author: Rickard Green
%%
-module(mtx_SUITE).

%%-define(line_trace,true).

-include_lib("common_test/include/ct.hrl").

-export([all/0,suite/0, init_per_suite/1, end_per_suite/1,
         init_per_testcase/2, end_per_testcase/2]).

-export([long_rwlock/1,
	 hammer_ets_rwlock/1,
	 hammer_rwlock/1,
	 hammer_rwlock_check/1,
	 hammer_tryrwlock/1,
	 hammer_tryrwlock_check/1,
	 hammer_sched_long_rwlock/1,
	 hammer_sched_long_rwlock_check/1,
	 hammer_sched_long_freqread_rwlock/1,
	 hammer_sched_long_freqread_rwlock_check/1,
	 hammer_sched_long_tryrwlock/1,
	 hammer_sched_long_tryrwlock_check/1,
	 hammer_sched_long_freqread_tryrwlock/1,
	 hammer_sched_long_freqread_tryrwlock_check/1,
	 hammer_sched_rwlock/1,
	 hammer_sched_rwlock_check/1,
	 hammer_sched_freqread_rwlock/1,
	 hammer_sched_freqread_rwlock_check/1,
	 hammer_sched_tryrwlock/1,
	 hammer_sched_tryrwlock_check/1,
	 hammer_sched_freqread_tryrwlock/1,
	 hammer_sched_freqread_tryrwlock_check/1]).

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap, {minutes, 15}}].

all() ->
    [long_rwlock, hammer_rwlock_check, hammer_rwlock,
     hammer_tryrwlock_check, hammer_tryrwlock,
     hammer_ets_rwlock, hammer_sched_long_rwlock_check,
     hammer_sched_long_rwlock,
     hammer_sched_long_freqread_rwlock_check,
     hammer_sched_long_freqread_rwlock,
     hammer_sched_long_tryrwlock_check,
     hammer_sched_long_tryrwlock,
     hammer_sched_long_freqread_tryrwlock_check,
     hammer_sched_long_freqread_tryrwlock,
     hammer_sched_rwlock_check, hammer_sched_rwlock,
     hammer_sched_freqread_rwlock_check,
     hammer_sched_freqread_rwlock,
     hammer_sched_tryrwlock_check, hammer_sched_tryrwlock,
     hammer_sched_freqread_tryrwlock_check,
     hammer_sched_freqread_tryrwlock].

init_per_suite(Config) when is_list(Config) ->
    DataDir = proplists:get_value(data_dir, Config),
    Lib = filename:join([DataDir, atom_to_list(?MODULE)]),
    case {erlang:load_nif(Lib, none),erlang:system_info(threads)} of
	{{error,_},false} ->
	    {skip, "No thread support"};
	_ ->
	    Config
    end.

end_per_suite(Config) when is_list(Config) ->
    catch erts_debug:set_internal_state(available_internal_state, false),
    Config.

init_per_testcase(_Case, Config) ->
    %% Wait for deallocations to complete since we measure
    %% runtime in test cases.
    wait_deallocations(),
    Config.

end_per_testcase(_Func, _Config) ->
    ok.

wait_deallocations() ->
    try
	erts_debug:set_internal_state(wait, deallocations)
    catch
	error:undef ->
	    erts_debug:set_internal_state(available_internal_state, true),
	    wait_deallocations()
    end.

long_rwlock(Config) when is_list(Config) ->
    statistics(runtime),
    LLRes = long_rw_test(),
    {_, RunTime} = statistics(runtime),
    %% A very short run time is expected, since
    %% threads in the test mostly wait
    io:format("RunTime=~p~n", [RunTime]),
    true = RunTime < 400,
    RunTimeStr = "Run-time during test was "++integer_to_list(RunTime)++" ms.",
    case LLRes of
	ok ->
	    {comment, RunTimeStr};
	{comment, Comment} ->
	    {comment, Comment ++ " " ++ RunTimeStr}
    end.

hammer_rwlock(Config) when is_list(Config) ->
    hammer_rw_test(false).

hammer_rwlock_check(Config) when is_list(Config) ->
    hammer_rw_test(true).

hammer_tryrwlock(Config) when is_list(Config) ->
    hammer_tryrw_test(false).

hammer_tryrwlock_check(Config) when is_list(Config) ->
    hammer_tryrw_test(true).

hammer_sched_rwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, false, true, 0, 0).

hammer_sched_rwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, true, true, 0, 0).

hammer_sched_freqread_rwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, false, true, 0, 0).

hammer_sched_freqread_rwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, true, true, 0, 0).

hammer_sched_tryrwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, false, false, 0, 100).

hammer_sched_tryrwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, true, false, 0, 100).

hammer_sched_freqread_tryrwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, false, false, 0, 100).

hammer_sched_freqread_tryrwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, true, false, 0, 100).

hammer_sched_long_rwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, false, true, 100, 0).

hammer_sched_long_rwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, true, true, 100, 0).

hammer_sched_long_freqread_rwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, false, true, 100, 0).

hammer_sched_long_freqread_rwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, true, true, 100, 0).

hammer_sched_long_tryrwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, false, false, 100, 100).

hammer_sched_long_tryrwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(false, true, false, 100, 100).

hammer_sched_long_freqread_tryrwlock(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, false, false, 100, 100).

hammer_sched_long_freqread_tryrwlock_check(Config) when is_list(Config) ->
    hammer_sched_rwlock_test(true, true, false, 100, 100).

hammer_sched_rwlock_test(FreqRead, LockCheck, Blocking, WaitLocked, WaitUnlocked) ->
    case create_rwlock(FreqRead, LockCheck) of
        enotsup ->
            {skipped, "Not supported."};
        RWLock ->
            Onln = erlang:system_info(schedulers_online),
            NWPs = case Onln div 3 of
                       1 -> case Onln < 4 of
                                true -> 1;
                                false -> 2
                            end;
                       X -> X
                   end,
            NRPs = Onln - NWPs,
            NoLockOps = ((((50000000 div Onln)
                           div case {Blocking, WaitLocked} of
                                   {false, 0} -> 1;
                                   _ -> 10
                               end)
                          div (case WaitLocked == 0 of
                                   true -> 1;
                                   false -> WaitLocked*250
                               end))
                         div handicap()),
            io:format("NoLockOps=~p~n", [NoLockOps]),
            Sleep = case Blocking of
                        true -> NoLockOps;
                        false -> NoLockOps div 10
                    end,
            WPs = lists:map(
                    fun (Sched) ->
                            spawn_opt(
                              fun () ->
                                      io:format("Writer on scheduler ~p.~n",
                                                [Sched]),
                                      Sched = erlang:system_info(scheduler_id),
                                      receive go -> gone end,
                                      hammer_sched_rwlock_proc(RWLock,
                                                               Blocking,
                                                               true,
                                                               WaitLocked,
                                                               WaitUnlocked,
                                                               NoLockOps,
                                                               Sleep),
                                      Sched = erlang:system_info(scheduler_id)
                              end,
                              [link, {scheduler, Sched}])
                    end,
                    lists:seq(1, NWPs)),
            RPs = lists:map(
                    fun (Sched) ->
                            spawn_opt(
                              fun () ->
                                      io:format("Reader on scheduler ~p.~n",
                                                [Sched]),
                                      Sched = erlang:system_info(scheduler_id),
                                      receive go -> gone end,
                                      hammer_sched_rwlock_proc(RWLock,
                                                               Blocking,
                                                               false,
                                                               WaitLocked,
                                                               WaitUnlocked,
                                                               NoLockOps,
                                                               Sleep),
                                      Sched = erlang:system_info(scheduler_id)
                              end,
                              [link, {scheduler, Sched}])
                    end,
                    lists:seq(NWPs + 1, NWPs + NRPs)),
            Procs = WPs ++ RPs,
            case {Blocking, WaitLocked} of
                {_, 0} -> ok;
                {false, _} -> ok;
                _ -> statistics(runtime)
            end,
            lists:foreach(fun (P) -> P ! go end, Procs),
            lists:foreach(fun (P) ->
                                  M = erlang:monitor(process, P),
                                  receive
                                      {'DOWN', M, process, P, _} ->
                                          ok
                                  end
                          end,
                          Procs),
            case {Blocking, WaitLocked} of
                {_, 0} -> ok;
                {false, _} -> ok;
                _ ->
                    {_, RunTime} = statistics(runtime),
                    io:format("RunTime=~p~n", [RunTime]),
                    true = RunTime < max(700, Onln*12),
                    {comment,
                     "Run-time during test was "
                     ++ integer_to_list(RunTime)
                     ++ " ms."}
            end
    end.

hammer_sched_rwlock_proc(_RWLock,
			 _Blocking,
			 _WriteOp,
			 _WaitLocked,
			 _WaitUnlocked,
			 0,
			 _Sleep) ->
    ok;
hammer_sched_rwlock_proc(RWLock,
			 Blocking,
			 WriteOp,
			 WaitLocked,
			 WaitUnlocked,
			 Times,
			 Sleep) when Times rem Sleep == 0 ->
    rwlock_op(RWLock, Blocking, WriteOp, WaitLocked, WaitUnlocked),
    hammer_sched_rwlock_proc(RWLock,
			     Blocking,
			     WriteOp,
			     WaitLocked,
			     WaitUnlocked,
			     Times - 1,
			     Sleep);
hammer_sched_rwlock_proc(RWLock,
			 Blocking,
			 WriteOp,
			 WaitLocked,
			 WaitUnlocked,
			 Times,
			 Sleep) ->
    rwlock_op(RWLock, Blocking, WriteOp, WaitLocked, 0),
    hammer_sched_rwlock_proc(RWLock,
			     Blocking,
			     WriteOp,
			     WaitLocked,
			     WaitUnlocked,
			     Times - 1,
			     Sleep).

-define(HAMMER_ETS_RWLOCK_REPEAT_TIMES, 1).
-define(HAMMER_ETS_RWLOCK_TSIZE, 500).

hammer_ets_rwlock(Config) when is_list(Config) ->
    {Ops, Procs} = case handicap() of
		       1 -> {20000, 500};
		       2 -> {20000, 50};
		       3 -> {2000, 50};
		       _ -> {200, 50}
		   end,
    io:format("Procs=~p~nOps=~p~n", [Procs, Ops]),
    lists:foreach(fun (XOpts) ->
			  io:format("Running with extra opts: ~p", [XOpts]),
			  hammer_ets_rwlock_test(XOpts, true, 2, Ops,
						 Procs, false)
		  end,
		  [[],
		   [{read_concurrency, true}],
		   [{write_concurrency, true}],
		   [{read_concurrency, true},{write_concurrency, true}]]),
    ok.

%% Aux funcs

long_rw_test() ->
    exit(no_nif_implementation).

hammer_rw_test(_Arg) ->
    exit(no_nif_implementation).

hammer_tryrw_test(_Arg) ->
    exit(no_nif_implementation).

create_rwlock(_FreqRead, _LockCheck) ->
    exit(no_nif_implementation).

rwlock_op(_RWLock, _Blocking, _WriteOp, _WaitLocked, _WaitUnlocked) ->
    exit(no_nif_implementation).

hammer_ets_rwlock_put_data() ->
    put(?MODULE, {"here are some", data, "to store", make_ref()}).

hammer_ets_rwlock_get_data() ->
    get(?MODULE).

hammer_ets_rwlock_ops(_T, _UW, _N, _C, _SC, 0) ->
    ok;
hammer_ets_rwlock_ops(T, UW, N, C, SC, Tot) when N >= ?HAMMER_ETS_RWLOCK_TSIZE ->
    hammer_ets_rwlock_ops(T, UW, 0, C, SC, Tot);
hammer_ets_rwlock_ops(T, UW, N, 0, SC, Tot) ->
    case UW of
	true ->
	    true = ets:insert(T, {N, Tot, hammer_ets_rwlock_get_data()});
	false ->
	    [{N, _, _}] = ets:lookup(T, N)
    end,
    hammer_ets_rwlock_ops(T, UW, N+1, SC, SC, Tot-1);
hammer_ets_rwlock_ops(T, UW, N, C, SC, Tot) ->
    case UW of
	false ->
	    true = ets:insert(T, {N, Tot, hammer_ets_rwlock_get_data()});
	true ->
	    [{N, _, _}] = ets:lookup(T, N)
    end,
    hammer_ets_rwlock_ops(T, UW, N+1, C-1, SC, Tot-1).

hammer_ets_rwlock_init(T, N) when N < ?HAMMER_ETS_RWLOCK_TSIZE ->
    ets:insert(T, {N, N, N}),
    hammer_ets_rwlock_init(T, N+1);
hammer_ets_rwlock_init(_T, _N) ->
    ok.

hammer_ets_rwlock_test(XOpts, UW, C, N, NP, SC) ->
    receive after 100 -> ok end,
    {TP, TM} = spawn_monitor(
                 fun () ->
                         _L = repeat_list(
                                fun () ->
                                        Caller = self(),
                                        T = fun () ->
                                                    Parent = self(),
                                                    hammer_ets_rwlock_put_data(),
                                                    T=ets:new(x, [public | XOpts]),
                                                    hammer_ets_rwlock_init(T, 0),
                                                    Ps0 = repeat_list(
                                                            fun () ->
                                                                    spawn_link(
                                                                      fun () ->
                                                                              hammer_ets_rwlock_put_data(),
                                                                              receive go -> ok end,
                                                                              hammer_ets_rwlock_ops(T, UW, N, C, C, N),
                                                                              Parent ! {done, self()},
                                                                              receive after infinity -> ok end
                                                                      end)
                                                            end,
                                                            NP - case SC of
                                                                     false -> 0;
                                                                     _ -> 1
                                                                 end),
                                                    Ps = case SC of
                                                             false -> Ps0;
                                                             _ -> [spawn_link(fun () ->
                                                                                      hammer_ets_rwlock_put_data(),
                                                                                      receive go -> ok end,
                                                                                      hammer_ets_rwlock_ops(T, UW, N, SC, SC, N),
                                                                                      Parent ! {done, self()},
                                                                                      receive after infinity -> ok end
                                                                              end) | Ps0]
                                                         end,
                                                    Start = erlang:monotonic_time(),
                                                    lists:foreach(fun (P) -> P ! go end, Ps),
                                                    lists:foreach(fun (P) -> receive {done, P} -> ok end end, Ps),
                                                    Stop = erlang:monotonic_time(),
                                                    lists:foreach(fun (P) ->
                                                                          unlink(P),
                                                                          exit(P, bang),
                                                                          M = erlang:monitor(process, P),
                                                                          receive
                                                                              {'DOWN', M, process, P, _} -> ok
                                                                          end
                                                                  end, Ps),
                                                    Res = (Stop-Start)/erlang:convert_time_unit(1,second,native),
                                                    Caller ! {?MODULE, self(), Res}
                                            end,
                                        TP = spawn_link(T),
                                        receive
                                            {?MODULE, TP, Res} ->
                                                Res
                                        end
                                end,
                                ?HAMMER_ETS_RWLOCK_REPEAT_TIMES)
                 end),
    receive
        {'DOWN', TM, process, TP, _} -> ok
    end.

repeat_list(Fun, N) ->
    repeat_list(Fun, N, []).

repeat_list(_Fun, 0, Acc) ->
    Acc;
repeat_list(Fun, N, Acc) ->
    repeat_list(Fun, N-1, [Fun()|Acc]).


handicap() ->
    X0 = case catch (erlang:system_info(logical_processors_available) >=
                     erlang:system_info(schedulers_online)) of
             true -> 1;
             _ -> 2
         end,
    case erlang:system_info(build_type) of
        opt ->
            X0;
        ReallySlow when ReallySlow == debug;
                        ReallySlow == valgrind ->
            X0*3;
        _Slow ->
            X0*2
    end.
